﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace AirlineServer
{
    internal partial class ClusterManager
    {
        private class Query
        {
            public AirlineClusterNode Node { get; set; }
            public string Server { get; set; }
            public string Location { get; set; }
            public DateTime From { get; set; }
            public DateTime To { get; set; }
            public FlightType FlightType { get; set; }
        }

        [MethodImpl(MethodImplOptions.Synchronized)] // don't allow calls while updating cluster/replication view
        public List<AirTravel> GetTravelOptions(string src, string dst, DateTime date, List<string> servers)
        {
            if (servers == null || servers.Count == 0)
            {
                // use all servers
                servers = GetAllServers();
            }

            var queries = new List<Query>();

            foreach (var server in servers)
            {
                var nodes = GetServerNodes(server);
                if (nodes.Count > 0)
                {
                    // query for flights: source => ?
                    queries.Add(new Query
                    {
                        Node = nodes.First(), // primary replica
                        Server = server,
                        Location = src,
                        From = date,
                        To = date,
                        FlightType = FlightType.Departing,
                    });

                    // query for flights: ? => destination
                    queries.Add(new Query
                    {
                        Node = nodes.Last(), // secondary replica
                        Server = server,
                        Location = dst,
                        From = date,
                        To = date.AddDays(1),
                        FlightType = FlightType.Arriving,
                    });
                }
            }

            var flights = QueryTravelOptions(queries, src, true);

            return BuildTravelOptions(flights, src, dst, date);
        }

        private List<Flight> QueryTravelOptions(List<Query> queries, string src, bool retryOnFail)
        {
            var failedQueries = new List<Query>();

            var flights = new List<Flight>();

            foreach (var query in queries)
            {
                try
                {
                    // in real applications, this would be asynchronous
                    var connection = clusterConnectionsCache.GetConnection(query.Node.Uri);
                    
                    var res = connection.Channel.Search(query.Server, query.Location, query.FlightType, query.From, query.To);

                    
                    if (query.FlightType == FlightType.Arriving)
                    {
                        // filter direct flights for FlightType=Arriving (we already get them in FlightType=Departing)
                        flights.AddRange(from flight in res
                                         where flight.From != src
                                         select flight);
                    }
                    else
                    {
                        flights.AddRange(res);
                    }
                }
                catch (Exception e)
                {
                    if (log.IsWarnEnabled)
                    {
                        log.Warn(string.Format("Failed to query node {0} ({1} server)", query.Node.ZkPath, query.Node.Name), e);
                    }

                    failedQueries.Add(query);
                }
            }

            if (retryOnFail)
            {
                queries = new List<Query>();

                foreach (var failedQuery in failedQueries)
                {
                    // let's try to run this again on the replicas
                    var replica = GetReplicaOfNode(failedQuery.Node);
                    if (replica != null)
                    {
                        failedQuery.Node = replica;
                        queries.Add(failedQuery);
                    }
                }

                flights.AddRange(QueryTravelOptions(queries, src, false));
            }

            return flights;
        }

        private List<AirTravel> BuildTravelOptions(List<Flight> flights, string src, string dst, DateTime date)
        {
            var travels = new List<AirTravel>();

			// add direct flights
            var res = from flight in flights
                                where flight.From == src &&
                                flight.To == dst &&
                                flight.Date == date
                                select flight;

            travels.AddRange(from flight in res
                             select new AirTravel
                             {
                                 Flights = new List<Flight>(new []
                                 {
                                     flight
                                 }),

                                 Price = flight.Price,
                             });

			// add connection flights
            var fromSrc = from flight in flights
                          where flight.From == src &&
                          flight.To != dst
                          select flight;

            foreach (var f in fromSrc)
            {
                var firstFlight = f;
                var connectionFlights = from flight in flights
                                        where flight.From == firstFlight.To &&
                                        flight.To == dst &&
                                        flight.Date >= firstFlight.Date
                                        select flight;

                travels.AddRange(from connectionFlight in connectionFlights
                                 select new AirTravel
                                 {
                                     Flights = new List<Flight>(new []
                                     {
                                         firstFlight,
                                         connectionFlight
                                     }),

                                     Price = firstFlight.Price + connectionFlight.Price,
                                 });
            }						  

            return travels;
        }

        private List<string> GetAllServers()
        {
            var replicas = from replica in ReplicationView
                           orderby replica.ServerName
                           select replica.ServerName;

            var distinct = replicas.Distinct();

            return distinct.ToList();
        }

        private List<AirlineClusterNode> GetServerNodes(string serverName)
        {
            var nodeNames = from replica in ReplicationView
                            where replica.ServerName == serverName
                            select replica.ReplicatingNode;

            var nodes = nodeNames.Select(
                name => ClusterView.FirstOrDefault(cn => cn.ZkPath == name)).
                        OfType<AirlineClusterNode>().
                        OrderBy(cn => cn.ZkPath);

            return nodes.ToList();
        }

        private AirlineClusterNode GetReplicaOfNode(AirlineClusterNode cn)
        {
            var res = from node in ClusterView
                      where node.Name == cn.Name && node.ZkPath != cn.ZkPath
                      select node;

            return res.FirstOrDefault() as AirlineClusterNode;
        }
    }
}
